import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import org.jetbrains.annotations.NotNull;
import static java.lang.foreign.MemorySegment.NULL;
import static java.lang.foreign.ValueLayout.*;
import static opus.opus_h.*;

/*
OPA format:
[3] "OPA"
[1] profile: 1:48kHz*1c; 2:48kHz*2c
[4] original frames(LE), not include beginning skip frames, packetCount = (originalFrames + skipFrames + 47999) / 48000
[1|2]*packetCount: delta encoding packet sizes
[nnn]*packetCount: opus encoding packets data
*/
public final class Sdl3Opa {
	private static final int SAMPLE_RATE = 48000;
	private static final int PACKET_FRAME_MS = 120;
	private static final int PACKET_FRAMES = SAMPLE_RATE * PACKET_FRAME_MS / 1000; // 5760
	private static final int TRIM_BEGIN_FRAMES = SAMPLE_RATE * 13 / 2000; // 312, 6.5ms
	private static final OfInt JAVA_INT_LE = JAVA_INT.withOrder(ByteOrder.LITTLE_ENDIAN);
	private static final @NotNull Arena arena = Arena.ofAuto();
	private static final @NotNull opus_decoder_ctl opus_decoder_ctl0 = opus_decoder_ctl.makeInvoker();

	static {
		// 确保提前加载opus库,大幅减少首次播放延迟
		//noinspection ResultOfMethodCallIgnored
		opus_decoder_create$descriptor();
		//noinspection ResultOfMethodCallIgnored
		opus_decode_float$descriptor();
		//noinspection ResultOfMethodCallIgnored
		opus_decoder_destroy$descriptor();
	}

	public static final class OpaSound implements Sdl3Audio.Sound {
		private final @NotNull String name;
		private final @NotNull MemorySegment fileMap;
		private final int channels;
		private final long totalFrames;
		private final long @NotNull [] packetOffsets;

		private static @NotNull MemorySegment mapFile(final @NotNull String filePath) throws IOException {
			try (final var raf = new RandomAccessFile(filePath, "r"); var fc = raf.getChannel()) {
				return fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size(), arena);
			}
		}

		public OpaSound(final @NotNull String filePath) throws IOException {
			this(mapFile(filePath), new File(filePath).getName());
		}

		public OpaSound(final @NotNull MemorySegment fileMap, final @NotNull String name) {
			this.name = name;
			this.fileMap = fileMap;
			final var fileSize = fileMap.byteSize();
			if (fileSize < 8)
				throw new UnsupportedOperationException("too small file: " + name);
			final int magic = fileMap.get(JAVA_INT_LE, 0);
			if ((magic & 0xffffff) != 0x41504f)
				throw new UnsupportedOperationException("unknown magic: " + name);
			channels = switch (magic >>> 24) {
				case '1' -> 1;
				case '2' -> 2;
				default -> throw new UnsupportedOperationException("unknown profile: " + name);
			};
			totalFrames = fileMap.get(JAVA_INT_LE, 4) & 0xffff_ffffL;
			final int packetCount = totalFrames > 0 ?
					(int)((TRIM_BEGIN_FRAMES + totalFrames + PACKET_FRAMES - 1) / PACKET_FRAMES) : 0;
			int lastPacketSize = 0;
			packetOffsets = new long[packetCount + 1];
			long p = 8;
			for (int i = 0; i < packetCount; i++) {
				if (p + 2 > fileSize)
					throw new IllegalStateException("truncated header");
				// 0000_0xxx xxxx_xxxx: 13-bit(BE) absolute size; other first byte: signed(b-0x84) [-124,123] is relative size to the last
				final int b = fileMap.get(JAVA_BYTE, p++) & 0xff;
				if (b > 7)
					lastPacketSize += b - 0x84;
				else
					lastPacketSize = (b << 8) + (fileMap.get(JAVA_BYTE, p++) & 0xff);
				if (lastPacketSize <= 0 || lastPacketSize > 0x7ff)
					throw new IllegalStateException("malformed header");
				packetOffsets[i] = lastPacketSize;
			}
			for (int i = 0; i < packetCount; i++) {
				final var packetSize = packetOffsets[i];
				packetOffsets[i] = p;
				p += packetSize;
			}
			packetOffsets[packetCount] = p;
			if (p > fileSize)
				throw new IllegalStateException("truncated header");
		}

		@Override
		public int channels() {
			return channels;
		}

		@Override
		public int sampleRate() {
			return SAMPLE_RATE;
		}

		@Override
		public long totalFrames() {
			return totalFrames;
		}

		@Override
		public @NotNull String name() {
			return name;
		}

		@Override
		public @NotNull Sdl3Audio.Decoder newDecoder() {
			return new OpaDecoder(this);
		}

		@Override
		public String toString() {
			return "{name=" + name + ",channels=" + channels + ",totalFrames=" + totalFrames + '}';
		}
	}

	public static final class OpaDecoder implements Sdl3Audio.Decoder {
		private final @NotNull OpaSound sound;
		private final @NotNull MemorySegment opusDecoder;
		private final @NotNull MemorySegment buf;
		private int nextPacket;

		public OpaDecoder(final @NotNull OpaSound sound) {
			this.sound = sound;
			final var errPtr = arena.allocate(JAVA_INT, 1);
			opusDecoder = opus_decoder_create(SAMPLE_RATE, sound.channels, errPtr);
			final int err = errPtr.get(JAVA_INT, 0);
			if (err != 0 || opusDecoder == NULL)
				throw new UnsupportedOperationException("opus_decoder_create failed: err=" + err + " for " + sound);
			buf = arena.allocate(PACKET_FRAMES * JAVA_FLOAT.byteSize() * 2, 32);
		}

		@Override
		public void close() {
			opus_decoder_destroy(opusDecoder);
		}

		@Override
		public int decode(long framePos, int maxFrames, final @NotNull MemorySegment dst, final int dstPos,
						  final float gainLeft, final float gainRight) {
			maxFrames = (int)Math.min(maxFrames, sound.totalFrames - framePos);
			if (maxFrames <= 0)
				return 0;
			framePos += TRIM_BEGIN_FRAMES;
			final int packet = Math.toIntExact(framePos / PACKET_FRAMES);
			final int packetOffset = (int)(framePos % PACKET_FRAMES);
			final int dstFrames = Math.min(PACKET_FRAMES - packetOffset, maxFrames);
			final int channels = sound.channels;
			final var srcFrameSize = 4L * channels;
			if (Sdl3Audio.DEBUG_LOG) {
				System.out.format("%d: framePos=%d=%d:%d, maxFrames=%d, nextPacket=%d, dstPos=%d, dstFrames=%d%n",
						System.nanoTime(), framePos, packet, packetOffset, maxFrames, nextPacket, dstPos, dstFrames);
			}
			if (packet == nextPacket - 1) {
				Sdl3Audio.Decoder.gainStereo(buf, packetOffset * srcFrameSize, dst, dstPos,
						channels, dstFrames, gainLeft, gainRight);
			} else {
				final var packetOffsets = sound.packetOffsets;
				if (packet != nextPacket) {
					if (packet == 0)
						opus_decoder_ctl0.apply(opusDecoder, 4028, (Object[])null); // OPUS_RESET_STATE=4028
					else {
						final var fileOffset = packetOffsets[packet - 1];
						opus_decode_float(opusDecoder, sound.fileMap.asSlice(fileOffset, 0),
								(int)(packetOffsets[packet] - fileOffset), buf, PACKET_FRAMES, 0);
					}
				}
				final var fileOffset = packetOffsets[packet];
				opus_decode_float(opusDecoder, sound.fileMap.asSlice(fileOffset, 0),
						(int)(packetOffsets[packet + 1] - fileOffset), buf, PACKET_FRAMES, 0);
				Sdl3Audio.Decoder.gainStereo(buf, packetOffset * srcFrameSize, dst, dstPos,
						channels, dstFrames, gainLeft, gainRight);
				nextPacket = packet + 1;
			}
			return dstFrames;
		}
	}
}
